#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
    Part of Seagull - The Pyhon Pages engine
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    WebPage:
      Abstract base class from which all the compiled pages shall inherit

      Creates more or less the (globals/locals) environment where pages shall
      run in. The globals are:
             - server: Server interface object
             - page: PageProxy object
                  * path
                  * meta
                  * call_placeholder()
                  * resolve_page()
                  * execute()
                  * try_execute()
                  * get_received_content()
		  * count_received_content()
             - params: ParamsProxy object
             - controls: ControlsProxy object
"""
__author__ = 'Jonathan Slenders <jonathan at slenders dot be>'
__license__ = 'GNU LGPL'
__all__ = ('Page', 'IncludeNotFoundException', 'PlaceholderNotImplementedException',
	'EntryPointException', 'MasterNotFoundException', 'ParameterException')

import posixpath
import sys
import __builtin__
import inspect

from utils import xml_escape
from url_translations import page_to_url
import page_lookup
import page_providers
import md5

class Page(object):
	"""
	All WebPages are based on this class.
	"""

	def __init__(self,
			server = None,
			path = u'/', # Unicode: path of this page
			external = False, # Boolean: externally accessible
			disabled = False, # True when execution is disabled
			master_path = None, # Unicode or None: path of this page's master
			child = None, # Instance of the child's WebPage
			caller = None, # Instance of the caller's WebPage
			meta = { }, # Dictionary of meta data
			instance_id = '', # Unique string for this page instance

			parameters = { }, # Dictionary of Parameter
			received_parameters = { }, # Received parameter values
			master_parameters = { }, # Dictionary of parameters to pass to the master page

			received_implementations = { }, # Dictionary of ContentBlock objects
			implementations_for_my_master = { },

			load_included_objects = (lambda: None), # Included objects loader

			init_block = None, # Function: Initialization block
			filters = [ ], # List of Filter
			content_type = None, # str: content type header in case of raw output
			output_mode = None): # str: 'xhtml', 'atom' or 'raw'

		self._included_object_id_counters = { }
		self._server = server
		self._implementations_for_my_master = implementations_for_my_master
		self._received_placeholder_implementations = received_implementations.copy()
		self._eventhandlers = [ ]
		self._destructors = [ ]
		self._loaded_pages = [ ]

		self.path = path
		self.external = external
		self.content_type = content_type
		self.output_mode = output_mode
		self.instance_id = instance_id
		self.instance_id_md5 = md5.new(instance_id.encode('utf-8')).hexdigest()

		# When a page is disabled, throw exception
		if disabled:
			raise PageIsDisabledException(path)

		# Environment where the code is executed within
		self.environment = { }

		# Proxy objects
		self.proxy = PageProxy()
		self.controls = ControlsProxy(path)
		self.params = ParametersProxy(parameters, path, self.environment)

		# Load included objects
		load_included_objects()

		# Wrap my own implementations
		for p in implementations_for_my_master:
			implementations_for_my_master[p].method = self._wrap_placeholder(p, implementations_for_my_master[p].method)

		# Load master page
		if master_path:
			try:
				self._master = self.load_python_page(
							self.resolve_page(master_path),
							master_parameters,
							implementations_for_my_master,
							child=self,
							caller=caller) # The master's caller is my caller

				# Inherit settings from master
				if self.output_mode == None:
					self.output_mode = self._master.output_mode

				if self.external == None:
					self.external = self._master.external

			except page_lookup.PageNotFoundException, e:
				raise MasterNotFoundException(e.path)
		else:
			self._master = None

		# Add filters
		for f in filters:
			server.add_filter(f.name, f.parameters)

		# Passing variables to proxy object
		self.proxy.meta = meta
		self.proxy.path = path
		self.proxy.instance_id = instance_id
		self.proxy.instance_id_md5 = self.instance_id_md5
		self.proxy.sys = sys

		self.proxy.controls = self.controls
		self.proxy.params = self.params
		self.proxy.resolve_page = self.resolve_page
		self.proxy.execute = self.execute
		self.proxy.try_execute = self.try_execute
		self.proxy.call_placeholder = self.call_placeholder
		self.proxy.try_call_placeholder = self.try_call_placeholder
		self.proxy.load = self.load
		self.proxy.get_received_content = self.get_received_content
		self.proxy.count_received_content = self.count_received_content
		self.proxy.register_locals = self.register_locals
		self.proxy.esc = self.esc
		self.proxy.event = self.event
		self.proxy.destructor= self.destructor
		self.proxy.environment = self.environment

		self.proxy.child = (child.proxy if child else None)
		self.proxy.master = (self._master.proxy if self._master else None)
		self.proxy.caller = (caller.proxy if caller else None)

		# Make exception classes available through the page object
		for e in __all__:
			if 'Exception' in e:
				self.proxy.__dict__[e] = eval(e)

		# Load query string parameters
		for key in server.query_parameters:
			if key in parameters and parameters[key].type == 'query-string':
				self.params._set_parameter_once(key, server.query_parameters[key])

		# Load parameters
		for key in received_parameters:
			if key in parameters:
				if parameters[key].type == 'set-once':
					self.params._set_parameter_once(key, received_parameters[key])
				elif parameters[key].type == 'read-write':
					self.params[key] = received_parameters[key]
				else:
					raise ParameterException(self.path, 'Attempt to change read-only parameter "%s"')
			else:
				raise ParameterException(self.path, 'Received unknown parameter "%s"' % key)

		# Environment
		self.register_locals ( {
			# Proxy objects
			'server': self._server,
			'page': self.proxy,
			'controls': self.controls,
			'params': self.params,

			# Shorthand for page methods
			'resolve_page': self.resolve_page,
			'esc': self.esc,
			'execute': self.execute,
			'sys': sys,
			'event': self.event,

			# Decorators
			'eventhandler': self.eventhandler,
			'destructor': self.destructor,
			} )
		self.register_locals( __builtin__.__dict__ )

		# Add controls to the environment
		for control in self.controls._get_all_proxies():
			self.environment[control] = self.controls[control]

		# Call initialisation block
		eval(init_block.func_code, self.environment)

		# Call eventhandlers
		for e in self._eventhandlers:
			query_parameters = self._server.query_parameters
			# Did this event happen?
			if 'event' in query_parameters and query_parameters['event'] == '%s:%s' % (self.instance_id_md5, e.name):
				# Params have instance_id_md5 as prefix
				params = map((lambda p: '%s:%s' % (self.instance_id_md5, p)), e.params)

				# Read query parameters
				params = [ (query_parameters[p] if p in query_parameters else '') for p in params ]

				# Call event handler
				e.func(*params)

	def dispose(self):
		"""
		Destructor
		"""
		# Call registered destructors
		for d in self._destructors:
			'call destructor %s' % self.path
			d()

		# Dispose all loaded pages (master, embeddde pages, loaded at runtime...)
		for e in self._loaded_pages:
			e.dispose()

	def register_locals(self, locals):
		"""
		Safe local variables in the environment scope
		"""
		self.environment.update(locals)

		# Always restore built in variables
		self.environment.update( {
			'__server__': self._server,
			'__page__': self.proxy,
			'__controls__': self.controls,
			'__params__': self.params,
			'__builtin__': __builtin__, } )

	def _wrap_placeholder(self, placeholder_name, placeholder_func):
		"""
		Wrap <pp:placeholder> tags around content block functions for this page and
		call this block through eval in the right local environment.
		"""
		def call():
			mark_placeholders = (self._server.output_mode != 'raw' and not self._server.has_output_buffer())

			if mark_placeholders:
				print ('<pp:placeholder name="%s" url-base="%s">' % (placeholder_name, self.path))

			eval(placeholder_func.func_code, self.environment)

			if mark_placeholders:
				print ('</pp:placeholder>')
		return call

	def _generate_instance_id(self, page_path, caller):
		"""
		Generate instance ID for child controls (append to caller's instance id)
		"""
		if caller:
			if page_path in self._included_object_id_counters:
				self._included_object_id_counters[page_path] += 1
			else:
				self._included_object_id_counters[page_path] = 0
			return "%s$%s[%s]" % (caller.instance_id, page_path, self._included_object_id_counters[page_path]);
		else:
			return None

	def load_python_page(self, page_path, parameters = { }, placeholder_implementations = { }, child=None, caller=None, instance_id=None):
		"""
		Load another page object. For internal use only
		"""
		assert(isinstance(page_path, unicode))
		assert(isinstance(parameters, dict))
		assert(page_path[0] == u'/')

		# Instance ID
		id = (instance_id if instance_id else self._generate_instance_id(page_path, caller))

		# Lookup page
		lookup = page_lookup.PageLookup()
		page = lookup.resolve_page(page_path)

		if page.url_suffix != '/' and len(page.url_suffix) > 0:
			raise Exception('No suffix allowed in internal includes (for path: %s, url-suffix %s)' %
					(page_path, page.url_suffix))

		if isinstance(page, page_providers.PythonPage):
			page_instance = lookup.initialize_page(page, self._server, placeholder_implementations, parameters, caller=caller, child=child, instance_id=id)
			self._loaded_pages.append(page_instance)
			return page_instance
		else:
			raise PageIsBinaryException(page_path)

	#########################################################
	###                  Decorators                       ###
	#########################################################
	def eventhandler(self, *p):
		""" Eventhandler decorator """
		def register_eventhandler(func):
			# Register eventhandler
			self._eventhandlers.append(EventHandler(func, name, params))

			# Return the original function, unmodified
			return func

		# Used as @eventhandler without arguments
		if len(p) == 1:
			assert inspect.isfunction(p[0])
			code = p[0].func_code
			(name, params) = (p[0].func_name, code.co_varnames[:code.co_argcount])
			return register_eventhandler(p[0])

		# Used as @eventhandler(name, params)
		elif len(p) == 2:
			assert isinstance(p[0], basestring)
			assert hasattr(p[1], '__iter__')
			(name, params) = (p[0], p[1])
			return register_eventhandler

	def destructor(self, func):
		""" Destructor decorator """
		assert inspect.isfunction(func)
		self._destructors.append(func)
		return func

	def event(self, name, **params):
		""" generate a query string to call this event """
		return u'?event=%s:%s&%s' % (self.instance_id_md5, name, u'&'.join(
					[ '%s:%s=%s'%(self.instance_id_md5,k,v) for k,v in params.iteritems() ] ))

	#########################################################
	### Methods below ale placed in the page proxy object ###
	#########################################################

	def execute(self, entry='Main'):
		"""
		Render this page, execute the 'Main' block. Another entry
		point can be passed through the parameter 'entry'
		Only a 'Main'-entry request will be passed on to the Master.
		"""
		assert isinstance(entry, basestring)

		if entry in self._implementations_for_my_master:
			self._implementations_for_my_master[entry].method ()
		elif entry == 'Main' and self._master:
			self._master.execute(entry)
		else:
			raise EntryPointException(self.path, entry)

	def try_execute(self, entry='Main'):
		"""
		Same as execute, but doesn't raise EntryPointExceptions
		when the entry doesn't exist. Returns True when executed
		successfully.
		"""
		try:
			self.execute(entry)
			return True
		except EntryPointException, e:
			return False

	def esc(self, *text):
		"""
		Escapes reserved XML characters like: <, > and &
		"""
		# TODO: Better move this to server_interface, instead of WebPage
		if len(text) == 1:
			if isinstance(text[0], basestring):
				return xml_escape(text[0])
			else:
				raise TypeError("Expected string (or tuple of strings)")
		elif len(text) == 0:
			return ()
		else:
			return tuple(self.esc(t) for t in text)


	def call_placeholder(self, placeholder_name):
		"""
		Calls a placeholder implementation.
		Will raise 'PlaceholderNotImplementedException' when we didn't receive an implementation.
		"""
		assert isinstance(placeholder_name, basestring)

		if placeholder_name in self._received_placeholder_implementations:
				self._received_placeholder_implementations[placeholder_name].method ()

		else:
			raise PlaceholderNotImplementedException(placeholder_name, self.path)

	def try_call_placeholder(self, placeholder_name):
		"""
		Same as call_placeholder.
		This function won't raise an exception when the placeholder wasn't implemented,
		but will return True on success.
		"""
		try:
			self.call_placeholder(placeholder_name)
			return True
		except PlaceholderNotImplementedException:
			return False

	def get_received_content(self):
		"""
		yields name, ContentBlock tuples for each received content block
		"""
		content = self._received_placeholder_implementations
		keys = content.keys()
		keys.sort();

		for k in keys:
			yield k, content[k]
	
	def count_received_content(self):
		"""
		Returns how many content blocks were received
		"""
		return len(self._received_placeholder_implementations)

	def resolve_page(self, page_path, reference_page=None):
		"""
		Transforms a page path relative to the current page,
		into an absolute page path.
		reference_path: relative to this page, in none is given, relative
		to the current page.

		Starting with /       -> relative to the pages root directory
		"""
		if not reference_page:
			reference_page = self.path

		return posixpath.normpath(posixpath.join(reference_page, u'..', page_path))

	def load(self, page_path, parameters = { }):
		"""
		Loads a page and returns the page proxy (user should render the
		page afterwards by calling .execute of the proxy)
		"""
		try:
			return self.load_python_page(page_path, parameters, caller=self).proxy

		except page_lookup.PageNotFoundException, e:
			raise IncludeNotFoundException(e.path)


############# Helper classes ###########


class Parameter(object):
	def __init__(self, name, classname, type, default_value, get_function=None, set_function=None):
		assert isinstance(name, str)
		assert classname in ('string', 'bool', 'int', 'float', 'object', 'function')
		assert type in ('query-string', 'set-once', 'read-only', 'read-write')
		assert isinstance(default_value, unicode)
		assert get_function == None or isinstance(get_function, basestring)
		assert set_function == None or isinstance(set_function, basestring)
		self.name = name
		self.classname = classname
		self.default_value = default_value
		self.type = type
		self.get_function = get_function
		self.set_function = set_function

class ContentBlock(object):
	def __init__(self, name, method, parameters = { }):
		self.name = name
		self.method = method
		self.parameters = parameters
	def execute(self):
		self.method()

class Filter(object):
	def __init__(self, name, parameters):
		self.name = name
		self.parameters = parameters

class PageProxy(object):
	"""
	Proxy object for the global 'page' variable in a python page.
	"""
	pass

class Control(object):
	"""
	Wrapper around embedded page instances.
	(loads page only after first referece is done.)
	"""
	class ControlParams(object):
		"""
		A wrapper around control.params. Never call the real
		control.params before any get or set is done, because it
		would cause initialisation of the whole control before needed.
		"""
		def __init__(self, control):
			self._control = control
		def __getattribute__(self, name):
			if name in ('_control'):
				return object.__getattribute__(self, name)
			else:
				self._control.load()
				return getattr(self._control.params, name)
		def __setattr__(self, name, value):
			if name in ('_control'):
				object.__setattr__(self, name, value)
			else:
				self._control.load()
				setattr(self._control.params, name, value)

	def __init__(self, page, path, params, placeholder_implementations):
		def loader():
			try:
				return page.load_python_page(self.path, params, placeholder_implementations, caller=page, instance_id=self._instance_id).proxy
			except page_lookup.PageNotFoundException, e:
				raise IncludeNotFoundException(e.path)

		self.path = path
		self._loader = loader
		self._control = None

		# We generate the instance ID already at this time to be sure that all child controls will
		# keep the same instance id independent from the "first reference"/initialization order.
		self._instance_id = page._generate_instance_id(path, page)

	def load(self):
		if not self._control:
			self._control = self._loader()

	def reload(self, path=None):
		"""
		Reinitialize control again
		'path': replace this page by this one with the same parameters
			and placeholders.
		"""
		if path:
			self.path = path

		self._control = self._loader()

	def __getattribute__(self, name):
		# Delegate private vars
		if name in ('__dict__', '__class__', '_loader', 'path', '_control', '_instance_id'):
			return object.__getattribute__(self, name)

		# Own public members
		elif name in ('load', 'reload'):
			return object.__getattribute__(self, name)

		# Params, instead of the real params, return the proxy (when the real control is not yet in memory)
		elif name in ('params') and not self._control:
			return Control.ControlParams(self)

		# Otherwise redirect to page object
		else:
			self.load()
			return getattr(self._control, name)

class ControlsProxy(object):
	"""
	Proxy object for accessing child controls (member of page instance)
	"""
	def __init__(self, path):
		self._controls = { } # Control proxy
		self._params = { }   # Control params proxy
		self._path = path

	def __iter__(self):
		"""
		Iterate through all controls
		"""
		for c in self._controls.itervalues():
			yield c

	def _get_all_proxies(self):
		for c in self._controls:
			yield c
		for c in self._params:
			yield c

	def __getitem__(self, name):
		if name in self._controls:
			return self._controls[name]
		elif name in self._params:
			return self._params[name]
		else:
			raise ControlException(self._path, name, 'No control named "%s"' % name)

	def _add_control(self, control, object_name, params_name):
		assert isinstance(control, Control)
		assert isinstance(object_name, basestring)
		assert params_name == None or isinstance(params_name, basestring)

		self._controls[object_name] = control
		if params_name:
			self._params[params_name] = control.params

	def __setattr__(self, name, value):
		"""
		This proxy is read only
		"""
		if name in ('_controls', '_params', '_path'):
			self.__dict__[name] = value
		else:
			raise ControlException(self._path, name, 'Controls proxy is read only')

	def __getattribute__(self, name):
		"""
		Capture all read access to this class
		"""
		# Delegate private vars
		if name in ('__dict__', '_controls', '_params', '_path', '_add_control', '_get_all_proxies', '__getitem__'):
			return object.__getattribute__(self, name)

		# Otherwise, load control
		return self.__getitem__(name)

class ParametersProxy(object):
	"""
	Proxy object for accessing parameters (member of page instance)
	"""
	def __init__(self, params, path, environment):
		self._params = params
		self._path = path
		self._environment = environment

		# Set default values
		for p in params:
			if not params[p].get_function:
				self.__dict__[p] = self._cast_parameter(p, params[p].default_value)

	def _set_parameter_once(self, name, value):
		""" Set read-only/set-once/query-string parameter values for the first time """
		# NOTE: they never have 'set'-functions
		self.__dict__[name] = self._cast_parameter(name, value)

	def _cast_parameter(self, name, value):
		""" Cast parameter to classname """
		classname = self._params[name].classname

		# Return value when the type matches
		if classname == 'string':
			if isinstance(value, basestring):
				return value

		elif classname == 'bool':
			if isinstance(value, bool):
				return value
			elif isinstance(value, basestring):
				if value.lower() in ('true', '1'):
					return True
				elif value.lower() in ('false', '0', ''):
					return False

		elif classname == 'int':
			if isinstance(value, int):
				return value
			elif value == '':
				return 0
			elif isinstance(value, basestring):
				try:
					return int(value)
				except ValueError:
					pass

		elif classname == 'float':
			if isinstance(value, float):
				return value
			elif value == '':
				return 0
			elif isinstance(value, basestring):
				try:
					return float(value)
				except ValueError:
					pass

		elif classname == 'object':
			if isinstance(value, object): # Probably returns always true
				return value

		elif classname == 'function':
			if inspect.isfunction(value):
				return value
			elif value == '':
				return (lambda: None)
	
		# Handle all invalid parameter values
		raise ParameterException(self._path,
				'Received non-%s value for parameter %s (value=%s)'  % (classname, name, str(value)))

	def __setattr__(self, name, value):
		"""
		Capture all write access to this class
		"""
		# Delegate private vars
		if name in ('_params', '_path', '_environment'):
			self.__dict__[name] = value

		# Only allow to get/set parameters which has been defined in the page
		elif name in self._params:
			# Cast value for parameters
			value = self._cast_parameter(name, value)

			# Use 'set'-function when some was defined,
			if self._params[name].set_function:
				set_function = self._params[name].set_function
				if set_function in self._environment:
					self._environment[set_function](value)
				else:
					raise ParameterException(self._path,
						"'set'-function for parameter '%s' does not exist in the page environment" % name)
			# Read only?
			elif self._params[name].type in ('read-only', 'query-string', 'set-once'):
				raise ParameterException(self._path, "parameter '%s' is read only" % name)
				
			# Otherwise, remember in __dict__
			else:
				self.__dict__[name] = value

		# Unknown param
		else:
			raise ParameterException(self._path, '%s is no valid parameter' % name)

	def __getattribute__(self, name):
		"""
		Capture all read access to this class
		"""
		# Delegate private vars
		if name in ('__dict__', '_params', '_path', '_environment', '_set_parameter_once', '_cast_parameter'):
			return object.__getattribute__(self, name)

		# Only allow to get/set parameters which has been defined in the page
		elif name in self._params:
			if self._params[name].get_function:
				get_function = self._params[name].get_function
				if get_function in self._environment:
					return self._cast_parameter(name, self._environment[get_function]())
				else:
					raise ParameterException(self._path,
						"'get'-function for parameter '%s' does not exist in the page environment" % name)
			else:
				return self._cast_parameter(name, self.__dict__[name])
		else:
			raise ParameterException(self._path, '%s is no valid parameter' % name)

	def __iter__(self):
		return self.__dict__.__iter__()

	def __getitem__(self, i):
		return self.__dict__[i]

	def __setitem__(self, i, value):
		setattr(self, i, value)


class EventHandler(object):
	def __init__(self, func, name, params):
		self.func = func
		self.name = name
		self.params = params

################## Exceptions ##############################

class PlaceholderNotImplementedException(Exception):
	"""
	Exception raised when a placeholder that should have been implemented
	by a master's child is missing.
	"""
	def __init__(self, placeholder_name, called_from):
		Exception.__init__(self, u'Placeholder "%s" not implemented (Called from %s)' %
				(placeholder_name, called_from))
		self.placeholder_name = placeholder_name

class EntryPointException(Exception):
	def __init__(self, path, entry_point):
		Exception.__init__(self, "No entry point '%s' in %s" % (entry_point, path))
		self.entry_point = entry_point
		self.path = path

class MasterNotFoundException(Exception):
	def __init__(self, path):
		Exception.__init__(self, 'Master page: %s not found' % path)
		self.path = path

class IncludeNotFoundException(Exception):
	def __init__(self, path):
		Exception.__init__(self, 'Included page: %s not found' % path)
		self.path = path

class ParameterException(Exception):
	def __init__(self, path, description):
		Exception.__init__(self, 'Page %s: %s' % (path, description))

class ControlException(Exception):
	def __init__(self, path, control, description):
		Exception.__init__(self, 'Page %s, Control %s: %s' % (path, control, description))


class PageIsBinaryException(Exception):
	def __init__(self, path):
		Exception.__init__(self, 'Page %s is a binary page, expected python page' % (path))
		self.path = path

class PageIsDisabledException(Exception):
	def __init__(self, path):
		Exception.__init__(self, 'Page %s is disabled, execution not allowed' % (path))
		self.path = path
